package cn.micro.eye.commons.auth;

import cn.micro.eye.commons.exception.MicroBadRequestException;
import cn.micro.eye.commons.exception.MicroErrorException;
import cn.micro.eye.commons.exception.MicroSignInException;
import cn.micro.eye.commons.utils.IPUtils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * Auth Context
 *
 * @author lry
 */
@Slf4j
public class AuthContext {

    public static final ThreadLocal<Object> CONTEXT = new InheritableThreadLocal<>();

    public static final String ATI_KEY = "ati";
    public static final String SCOPE_TYPE = "all";
    public static final String BEARER_TYPE = "Bearer";
    public static final String[] SCOPE_TYPES = {SCOPE_TYPE};
    public static final String ACCESS_TOKEN_KEY = "TokenBody";
    public static final String ACCESS_TOKEN_QUERY_KEY = "access_token";
    public static final String ACCESS_TOKEN_HEADER_KEY = "X-Access-Token";
    public static final String REFRESH_TOKEN_HEADER_KEY = "X-Refresh-Token";

    private static final String ISSUER = "lry";
    private static final String SECRET = "micro-lry";

    private static Algorithm ALGORITHM;
    private static JWTVerifier VERIFIER;
    private static Long tokenExpires = 7200L;
    private static Long refreshTokenExpires = 86400L;

    static {
        try {
            ALGORITHM = Algorithm.HMAC256(SECRET);
            VERIFIER = JWT.require(ALGORITHM).build();
        } catch (UnsupportedEncodingException e) {
            log.error(e.getMessage(), e);
        }
    }

    public static void initialize(Long tokenExpires, Long refreshTokenExpires) {
        AuthContext.tokenExpires = tokenExpires;
        AuthContext.refreshTokenExpires = refreshTokenExpires;
    }

    public static MicroToken build(TokenBody tokenBody) {
        return build(tokenBody.getUserId(), tokenBody.getUserName(),
                tokenBody.getClientId(), tokenBody.getAuthorities(), tokenBody.getOthers());
    }

    public static MicroToken build(Long userId, String userName, String clientId,
                                   List<String> authorities, Map<String, Object> others) {
        try {
            String jti = UUID.randomUUID().toString();

            JWTCreator.Builder builder = build(others);
            builder.withJWTId(jti);
            builder.withIssuer(ISSUER);
            builder.withArrayClaim(MicroToken.SCOPE, SCOPE_TYPES);
            builder.withExpiresAt(new Date(System.currentTimeMillis() + tokenExpires * 1000));

            builder.withClaim(TokenBody.USER_NAME, userName);
            builder.withClaim(TokenBody.CLIENT_ID, clientId);
            withObjectClaim(builder, TokenBody.USER_ID, userId);
            if (!(authorities == null || authorities.size() == 0)) {
                builder.withArrayClaim(TokenBody.AUTHORITIES, authorities.toArray(new String[0]));
            }

            String accessTokenStr = builder.sign(ALGORITHM);
            builder.withClaim(ATI_KEY, UUID.randomUUID().toString());
            builder.withExpiresAt(new Date(System.currentTimeMillis() + refreshTokenExpires * 1000));
            String refreshTokenStr = builder.sign(ALGORITHM);

            return new MicroToken(accessTokenStr, BEARER_TYPE, tokenExpires, refreshTokenStr, SCOPE_TYPE, jti);
        } catch (JWTCreationException e) {
            throw new MicroErrorException(e.getMessage(), e);
        }
    }

    public static void verify(String token) {
        if (StringUtils.isBlank(token)) {
            throw new MicroBadRequestException("Not found token");
        }

        try {
            VERIFIER.verify(token);
        } catch (TokenExpiredException e) {
            throw new MicroSignInException("Token has expired");
        } catch (Exception e) {
            throw new MicroSignInException(HttpStatus.FORBIDDEN.value(), "Signature verification exception");
        }
    }

    private static JWTCreator.Builder build(Map<String, Object> others) {
        JWTCreator.Builder builder = JWT.create();
        if (!(others == null || others.isEmpty())) {
            for (Map.Entry<String, Object> entry : others.entrySet()) {
                withObjectClaim(builder, entry.getKey(), entry.getValue());
            }
        }

        return builder;
    }

    static TokenBody parseToken(String token) {
        if (token == null || token.length() == 0) {
            throw new MicroBadRequestException("Token can not be empty");
        }

        try {
            DecodedJWT decodedJWT = JWT.decode(token);
            TokenBody tokenBody = new TokenBody();
            tokenBody.setUserId(decodedJWT.getClaim(TokenBody.USER_ID).asLong());
            tokenBody.setUserName(decodedJWT.getClaim(TokenBody.USER_NAME).asString());
            tokenBody.setClientId(decodedJWT.getClaim(TokenBody.CLIENT_ID).asString());
            tokenBody.setAuthorities(decodedJWT.getClaim(TokenBody.AUTHORITIES).asList(String.class));
            tokenBody.setOthers(decodedJWT.getClaim(TokenBody.OTHERS).asMap());
            return tokenBody;
        } catch (Exception e) {
            throw new MicroBadRequestException("Decode is exception");
        }
    }

    public static TokenBody getContextAccessToken() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            throw new MicroErrorException("Not Found RequestAttributes");
        }

        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        return (TokenBody) request.getAttribute(ACCESS_TOKEN_KEY);
    }

    public static Long getUserId() {
        TokenBody tokenBody = getContextAccessToken();
        return tokenBody.getUserId();
    }

    public static List<String> getAuthorities() {
        TokenBody tokenBody = getContextAccessToken();
        return tokenBody.getAuthorities();
    }

    public static void setOperator(String operation) {
        CONTEXT.remove();
        CONTEXT.set(operation);
    }

    public static void removeOperator() {
        CONTEXT.remove();
    }

    public static String getOperator() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes != null) {
            if (requestAttributes instanceof ServletRequestAttributes) {
                HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
                Object tokenBody = request.getAttribute(ACCESS_TOKEN_KEY);
                if (tokenBody instanceof TokenBody) {
                    Long userId = ((TokenBody) tokenBody).getUserId();
                    if (userId != null) {
                        return String.valueOf(userId);
                    }
                }

                String ip = IPUtils.getIPAddress(request);
                if (!(ip == null || ip.length() == 0)) {
                    return ip;
                }
            }
        }

        return (CONTEXT.get() != null) ? ("sys:" + CONTEXT.get()) : "sys";
    }

    private static void withObjectClaim(JWTCreator.Builder builder, String key, Object value) {
        if (value instanceof Integer) {
            builder.withClaim(key, (Integer) value);
        } else if (value instanceof Boolean) {
            builder.withClaim(key, (Boolean) value);
        } else if (value instanceof Date) {
            builder.withClaim(key, (Date) value);
        } else if (value instanceof Long) {
            builder.withClaim(key, (Long) value);
        } else if (value instanceof Double) {
            builder.withClaim(key, (Double) value);
        } else if (value instanceof String) {
            builder.withClaim(key, (String) value);
        } else if (value instanceof Long[]) {
            builder.withArrayClaim(key, (Long[]) value);
        } else if (value instanceof String[]) {
            builder.withArrayClaim(key, (String[]) value);
        } else if (value instanceof Integer[]) {
            builder.withArrayClaim(key, (Integer[]) value);
        } else {
            throw new IllegalArgumentException("Unsupported Data Types");
        }
    }

}
